package com.unknow.first.mfa.service;

import cn.hutool.core.util.StrUtil;
import com.unknow.first.util.GoogleAuthenticatorUtil;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base32;
import org.cloud.constant.CoreConstant.OperateLogType;
import org.cloud.constant.MfaConstant;
import org.cloud.encdec.service.AESService;
import org.cloud.exception.BusinessException;
import org.cloud.logs.annotation.AuthLog;
import org.cloud.utils.NumericUtil;
import org.cloud.utils.SpringContextUtil;
import org.springframework.http.HttpStatus;


public class GoogleAuthenticatorService {
    // 最多可偏移的时间
    int window_size = 1; // default 3 - max 17
    @AuthLog(bizType = "framework", desc = "校验google验证码", operateLogType = OperateLogType.LOG_TYPE_BACKEND)
    public Boolean checkGoogleVerifyCode(final String googleSecretEnc, final String mfaValue) throws BusinessException {
        AESService aesService = SpringContextUtil.getBean(AESService.class);
        final String googleSecret;
        try {
            googleSecret = aesService.decrypt(googleSecretEnc);
        } catch (Exception e) {
            throw new BusinessException(e.getMessage(), e, 401);
        }

        // 请输入谷歌验证码
        if (StrUtil.isEmpty(mfaValue)) {
            throw new BusinessException(MfaConstant.CORRELATION_GOOGLE_VERIFY_CODE_ISNULL.value(),
                MfaConstant.CORRELATION_GOOGLE_VERIFY_CODE_ISNULL.description(), HttpStatus.BAD_REQUEST.value());
        }
        // 请输入谷歌验证码
        if (!NumericUtil.single().isLong(mfaValue)) {
            throw new BusinessException(MfaConstant.CORRELATION_GOOGLE_VERIFY_CODE_NOT_NUMERIC.value(),
                MfaConstant.CORRELATION_GOOGLE_VERIFY_CODE_NOT_NUMERIC.description(), HttpStatus.BAD_REQUEST.value());
        }

        boolean isVerifyPass = GoogleAuthenticatorUtil.single()
            .checkCode(googleSecret, Long.parseLong(mfaValue), System.currentTimeMillis());

        if (!isVerifyPass) {
            throw new BusinessException(MfaConstant.CORRELATION_GOOGLE_VERIFY_FAILED.value(),
                MfaConstant.CORRELATION_GOOGLE_VERIFY_FAILED.description(),
                HttpStatus.BAD_REQUEST.value());
        }
        return true;
    }

    /**
     * Check the code entered by the user to see if it is valid 验证code是否合法
     *
     * @param secret   The users secret.
     * @param code     The code displayed on the users device
     * @param timeMsec The time in msec (System.currentTimeMillis() for example)
     * @return
     */
    public boolean checkCode(String secret, long code, long timeMsec) {
        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(secret);
        long t = (timeMsec / 1000L) / 30L;
        for (int i = -window_size; i <= window_size; ++i) {
            long hash;
            try {
                hash = verifyCode(decodedKey, t + i);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            if (hash == code) {
                return true;
            }
        }
        return false;
    }

    private int verifyCode(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
        byte[] data = new byte[8];
        long value = t;
        for (int i = 8; i-- > 0; value >>>= 8) {
            data[i] = (byte) value;
        }
        SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(signKey);
        byte[] hash = mac.doFinal(data);
        int offset = hash[20 - 1] & 0xF;
        // We're using a long because Java hasn't got unsigned int.
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
            truncatedHash <<= 8;
            // We are dealing with signed bytes:
            // we just keep the first byte.
            truncatedHash |= (hash[offset + i] & 0xFF);
        }
        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;
        return (int) truncatedHash;
    }
}
